using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;

using Atomic.Libraries;
using Atomic.Libraries.Mathematics;
using Atomic.Structures;

namespace Atomic.Clusters
{
	[Serializable]
	public class ClusterFunctionEvaluator : IClusterFunctionEvaluator
	{
		private bool intensive;
		private InnerEvaluator evaluator;

		public ClusterFunctionEvaluator(Structure lattice)
			: this(lattice, false)
		{
		}

		public ClusterFunctionEvaluator(Structure lattice, bool intensive)
			: this(lattice, intensive, false)
		{
		}

		public ClusterFunctionEvaluator(Structure lattice, bool intensive, bool random)
			: this(new InnerEvaluator(lattice, random), intensive)
		{
		}

		private ClusterFunctionEvaluator(InnerEvaluator evaluator, bool intensive)
		{
			this.evaluator = evaluator;
			this.intensive = intensive;
		}

		public IClusterFunctionValue Evaluate(Structure structure, IEnumerable<ClusterClass> clusters)
		{
			if (structure == null || clusters == null)
			{
				throw new ArgumentNullException();
			}

			if (intensive)
			{
				// Override computed scaling for intensive properties.
				return new ClusterFunctionValue(evaluator.Evaluate(structure, clusters).Correlations, 1);
			}
			else
			{
				return evaluator.Evaluate(structure, clusters);
			}
		}

		public IEnumerable<IClusterFunctionValue> Evaluate(IEnumerable<Structure> structures, IEnumerable<ClusterClass> clusters)
		{
			// Currently no efficient method implemented.
			foreach (Structure structure in structures)
			{
				yield return Evaluate(structure, clusters);
			}
		}

		public ClusterFunctionEvaluator IntensiveEvaluator
		{
			get
			{
				if (intensive)
				{
					// Already an intensive evaluator.
					return this;
				}

				return new ClusterFunctionEvaluator(evaluator, true);
			}
		}
			
		public ClusterFunctionEvaluator ExtensiveEvaluator
		{
			get
			{
				if (!intensive)
				{
					// Already an extensive evaluator.
					return this;
				}

				return new ClusterFunctionEvaluator(evaluator, false);
			}
		}

		[Serializable]
		private class InnerEvaluator
		{
			private Structure lattice;
			private bool random;
			private RollingCache<Structure, Tuple<Dictionary<ClusterClass, double>, int>> values;

			public InnerEvaluator(Structure lattice, bool random)
			{
				if (lattice == null)
				{
					throw new ArgumentNullException();
				}

				this.lattice = lattice;
				this.random = random;

				// Keep the values of the correlation function (and the scaling) of no more than 1000 structures. If the structures are
				// chosen indefinitely at random, this avoid filling the memory.
				values = new RollingCache<Structure, Tuple<Dictionary<ClusterClass, double>, int>>(1000);
			}
			
			public IClusterFunctionValue Evaluate(Structure structure, IEnumerable<ClusterClass> clusters)
			{
				Dictionary<ClusterClass, double> correlations;
				int scaling;

				Tuple<Dictionary<ClusterClass, double>, int> value;
				if (values.TryGetValue(structure, out value))
				{
					correlations = value.Item1;
					scaling = value.Item2;
				}
				else
				{
					correlations = new Dictionary<ClusterClass, double>();
					scaling = -1;
				}

				// Exclude already evaluated clusters.
				List<ClusterClass> clusters0 = new List<ClusterClass>();
				foreach (ClusterClass cluster in clusters)
				{
					if (!correlations.ContainsKey(cluster))
					{
						clusters0.Add(cluster);
					}
				}

				int n = clusters0.Count;
				if (n > 0)
				{
					// Do this only of some clusters need to be evaluated.
					double[] correlations0;
					int scaling0;
					Evaluate(structure, clusters0, n, out correlations0, out scaling0);

					// Then add the just calculated correlations to the dictionary.
					for (int i = 0; i < n; i++)
					{
						correlations[clusters0[i]] = correlations0[i];
					}

					if (scaling < 0)
					{
						// Not already computed. Update with just computed scaling.
						scaling = scaling0;
					}
					else if(scaling != scaling0)
					{
						throw new Exception("Scaling inconsistent with previously computed value.");
					}
				}

				// Keep in the rolling cache by adding it again.
				values.Add(structure, Tuple.Create<Dictionary<ClusterClass, double>, int>(correlations, scaling));

				List<double> correlations2 = new List<double>();
				foreach (ClusterClass cluster in clusters)
				{
					correlations2.Add(correlations[cluster]);
				}

				return new ClusterFunctionValue(correlations2, scaling);
			}
			
			private void Evaluate(Structure structure, IEnumerable<ClusterClass> clusters, int n, out double[] correlations, out int scaling)
			{
				using (TempDirectory temp = new TempDirectory())
				{
					lattice.Write(temp.Directory.AddFile("lat.in"));
					structure.Write(temp.Directory.AddFile("str.out"));
					new ClusterFinder(lattice).Write(temp.Directory.AddFile("clusters.out"), clusters);

					// Start corrdump process. Use predefined clusters. IEEE 754 double-precision has up to 17 significant digits.
					Process p = new Process();
					p.StartInfo.RedirectStandardOutput = true;
					p.StartInfo.RedirectStandardError = true;
					p.StartInfo.UseShellExecute = false;
					p.StartInfo.FileName = "corrdump";
					p.StartInfo.Arguments = "-c -sig=17 -wu=str_unrelaxed.out" + (random ? " -rnd" : "");
					p.StartInfo.WorkingDirectory = temp.Directory.FullName;
					p.Start();
					p.WaitForExit();

					if (p.ExitCode != 0)
					{
						throw new Exception("Running corrdump failed.");
					}

					if (p.StandardError.ReadToEnd().Contains("Unknown atom label:"))
					{
						// Don't allow unknown atoms.
						throw new Exception("Structure contains unknown atoms.");
					}

					string s = p.StandardOutput.ReadToEnd();
					correlations = AtomicEnvironment.ParseArray(s.Replace("\t", " ").Replace("\n", " "));
					if (correlations.Length != n)
					{
						throw new Exception("Unexpected corrdump output.");
					}

					// Read strain (and structure-lattice mapping?) from the log file.
					string[] log = File.ReadAllLines(temp.Directory.AddFile("corrdump.log").FullName);
					SpaceMatrix strain = AtomicEnvironment.ParseSpaceMatrix(log[1], log[2], log[3]);

					// Or read unrelaxed (ideal) structure from str_unrelaxed.out. Maybe too slow to parse the structure?
					Structure unrelaxedStructure = Structure.Read(temp.Directory.AddFile("str_unrelaxed.out"));

					// Determine number of sites including omitted vacancies in the structure.
					int size = unrelaxedStructure.Sites.Count;

					double c = (Math.Abs(strain[0, 0]) + Math.Abs(strain[1, 1]) + Math.Abs(strain[2, 2])) / 3.0;
					if (SpaceMatrix.Norm(strain - SpaceMatrix.Identity * c) > 1.0e-5)
					{
						//Structure.Show(lattice, structure, unrelaxedStructure);
						throw new Exception("Larger than expected strain in structure-lattice mapping.");
					}

					if (size % lattice.Sites.Count != 0)
					{
						// This also happens if vacancies have been removed.
						throw new Exception("Invalid scaling of the structure.");
					}

					// The scaling is a by-product of the cluster evaluation. Store it for later use.
					scaling = size / lattice.Sites.Count;
				}
			}
		}

		[Serializable]
		private class ClusterFunctionValue : IClusterFunctionValue
		{
			public ClusterFunctionValue(IEnumerable<double> correlations, int scaling)
			{
				Correlations = correlations;
				Scaling = scaling;
			}

			public IEnumerable<double> Correlations
			{
				get;
				private set;
			}

			public int Scaling
			{
				get;
				private set;
			}
		}
	}
}
